package edu.utdallas.rxs104020;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Properties;
import java.util.concurrent.PriorityBlockingQueue;

import org.apache.log4j.Logger;

/* 1. make sure messages are received only by the server part of the client.
 * 2. all modifiable state variables are synchronized.
 */

/*
 * NOTE 
 * messageType 1 - Request
 * messageType 2 - Reply
 * messageType 3 - Release
 */
public class Cliente {

	private static Logger log = Logger.getLogger(Cliente.class);
	private int localTime = 0;
	private int offset;
	private int id;
	private static Properties config = new Properties();
	private int listenPort;
	private ServerSocket serverSocket;
	private static Map<String, PriorityBlockingQueue<QueueElement>> map = new HashMap<String, PriorityBlockingQueue<QueueElement>>();
	//private PriorityBlockingQueue<QueueElement> localQ = new PriorityBlockingQueue<QueueElement>();
	private boolean[] repliedArray = { false, false, false, false, false };
	private static List<String> fileNames = new ArrayList<String>();
	private static boolean allReqSent = false;
	private int lastReqTime = 0;
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		try {
			final Cliente myClient = new Cliente();

			config = new Properties();
			// config.load(Run.class.getResourceAsStream("resources/config.properties"));
			config.load(new FileInputStream("config.properties"));
			myClient.offset = Integer.parseInt(config.getProperty("OFFSET"));
			myClient.id = Integer.parseInt(config.getProperty("ID"));
			/*********************** ENQUIRE ***********************************/
			myClient.contactServer(config.getProperty("SERVER1"),
					Integer.parseInt(config.getProperty("SERVER1_PORT")),
					myClient.id + ":" + 3, 3);
			
			/*******************************************************************/

			/*********************** START SERVER AS SEPARATE THREAD ************************/
			Runnable server = new Runnable() {
				public void run() {
					myClient.startUp();
				}
			};

			Thread serverThread = new Thread(server);
			serverThread.start();

			/********************** SERVER STARTED *********************************************/

			/********************** NOW START THE CLIENT REQUESTS ****************************/
			for (int i = 0; i < 10; i++) {
				// message sending is an event so increment the local clock by
				// offset
				int fileNum = getRandom(fileNames.size()) - 1;
				String fileName = fileNames.get(fileNum);
				
				
				System.out.println("Sending Req...");
				synchronized (myClient) {
					myClient.incrementLocalClock();
					allReqSent = false;
					for (int j = 1; j < 6; j++) {
						if (j == myClient.id)
							continue;
						myClient.sendMessage(
								config.getProperty("CLIENT" + j),
								Integer.parseInt(config.getProperty("CLIENT"
										+ j + "_PORT")), 4, fileName);
						// once you send a new request, update booleans to false
						myClient.repliedArray[Integer.parseInt(config
								.getProperty("CLIENT" + j + "_ID")) - 1] = false;

					}
					allReqSent = true;
					myClient.notifyAll();
				}
				// add own request to q
				map.get(fileName).add(new QueueElement(myClient.id,
						myClient.lastReqTime));
				myClient.repliedArray[myClient.id - 1] = true;

				// check if all messages are received
				while (!myClient.checkAllReplied()) {
					// blocking call. wait till everyone replies
				}
				// System.out.println("after boolean check");
				for (QueueElement q : map.get(fileName)) {
					System.out.println(q.getId() + "-->" +q.getTimeStamp());
				}
				// if yes check if it is your turn
				// if yes go for critical section - dont forget to set the flag
				while(map.get(fileName).peek().getId() != myClient.id) {
					Thread.sleep(100);
				}
				System.out.println("I am ahead of Q");
				
				int reqType = 2;// getRandom(2);// either read or write

				if (reqType == 1) {
					int serverNo = getRandom(3);// any of 3 servers
					myClient.contactServer(
							config.getProperty("SERVER" + serverNo),
							Integer.parseInt(config.getProperty("SERVER"
									+ serverNo + "_PORT")), myClient.id + ":"
									+ reqType + ":" + fileNames.get(fileNum),
							reqType);
				} else {
					for (int i1 = 1; i1 < 4; i1++) {
						myClient.contactServer(
								config.getProperty("SERVER" + i1),
								Integer.parseInt(config.getProperty("SERVER"
										+ i1 + "_PORT")),
								myClient.id + ":" + reqType + ":"
										+ fileNames.get(fileNum) + ":"
										+ myClient.lastReqTime, reqType);
					}
				}
				synchronized(myClient){
				myClient.incrementLocalClock();
				for (int j = 1; j < 6; j++) {
					if (j == myClient.id)
						continue;
					myClient.sendMessage(
							config.getProperty("CLIENT" + j),
							Integer.parseInt(config.getProperty("CLIENT" + j
									+ "_PORT")), 6, fileName);
				}
				// myClient.localQ.remove();
				map.get(fileName).poll();
				System.out.println("Sent Release at : " + myClient.localTime);
				myClient.notifyAll();
				}

				 try { Thread.sleep(Math.round(Math.random()) * 1000); } catch
				 (InterruptedException e) { 
				 e.printStackTrace(); }

			}
		} catch (IOException e) {
			log.error("Error finding/loading configuration file", e);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}

	private void contactServer(String host, int port, String msg, int msgType) {
		// Create socket and streams
		Socket socket = null;
		PrintWriter clOut;
		BufferedReader clIn;

		try {
			socket = new Socket(host, port);
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		try {
			clOut = new PrintWriter(socket.getOutputStream(), true);
			clIn = new BufferedReader(new InputStreamReader(
					socket.getInputStream()));

			clOut.println(msg);
			// get the response
			String response = null;
			while ((response = clIn.readLine()) != null
					&& !response.equals("EOM")) {
				switch (msgType) {
				case 1:
					break;
				case 2:
					break;
				case 3:
					String[] temp = response.split(",");
					for (String t : temp) {
						fileNames.add(t);
						map.put(t, new PriorityBlockingQueue<QueueElement>());
					}
				}

			}

			// close all streams & sockets
			clOut.close();
			clIn.close();
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	private synchronized void incrementLocalClock() {
		this.localTime += this.offset;
	}

	private synchronized void updateLocalClock(int incomingTime) {
		if (this.localTime < incomingTime) {
			this.localTime = incomingTime;
		}
	}

	private void startUp() {
		log.info("Starting Up Server for Process" + this.id);

		this.listenPort = Integer.parseInt(config.getProperty("LISTEN_PORT"));
		// create a server socket
		try {
			this.serverSocket = new ServerSocket(this.listenPort);
			while (true) {
				final Socket responseSocket = this.serverSocket.accept();
				Runnable clientJob = new Runnable() {
					public void run() {
						handleRequest(responseSocket);
					}
				};

				Thread clientThread = new Thread(clientJob);
				clientThread.start();

			}
		} catch (IOException e) {
			log.error("Error with Server Socket Creation", e);
		}
	}

	private void handleRequest(Socket responseSocket) {
		try {
			BufferedReader serIn = new BufferedReader(new InputStreamReader(
					responseSocket.getInputStream()));

			String incomingMsg = null;
			synchronized (this) {
				while (!allReqSent) {
					this.wait();
				}
			}

			while ((incomingMsg = serIn.readLine()) != null
					&& !incomingMsg.equals("EOM")) {

				String[] temp = incomingMsg.split(":");
				int reqId = Integer.parseInt(temp[0]);
				int reqType = Integer.parseInt(temp[1]);
				int reqTimeStamp = Integer.parseInt(temp[2]);
				String fileName = temp[3];
				this.updateLocalClock(reqTimeStamp + offset);
				// take action according to message
				switch (reqType) {
				case 4:
					map.get(fileName).add(new QueueElement(reqId, reqTimeStamp));
					// update booleans
					// if (reqTimeStamp > this.lastReqTime) {
					// this.repliedArray[reqId - 1] = true;
					// }
					this.incrementLocalClock();
					// send a reply to that client's server
					this.sendMessage(
							config.getProperty("CLIENT" + reqId),
							Integer.parseInt(config.getProperty("CLIENT"
									+ reqId + "_PORT")), 5, null);

					break;

				case 5:
					// update booleans
					System.out.println("Request was made at: "
							+ this.lastReqTime);
					System.out.println("Reply from " + reqId + " received at: "
							+ reqTimeStamp);
					if (reqTimeStamp > this.lastReqTime) {
						this.repliedArray[reqId - 1] = true;
						// replyForReq = true;

					}

					break;

				case 6:
					// remove corresponding message from q.
					synchronized(this){ 
					map.get(fileName).poll();
					// if (reqTimeStamp > this.lastReqTime) {
					// this.repliedArray[reqId - 1] = true;
					// }
					this.notifyAll();
					}
					System.out.println("received release msg from " + reqId);
					break;
				}

			}

			serIn.close();
		} catch (IOException e) {
			log.error("Error in handling requests", e);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}

	private synchronized void sendMessage(String host, int port, int msgType, String fileName) {
		try {

			// Create socket and streams
			Socket socket;
			PrintWriter clOut;
			socket = new Socket(host, port);
			clOut = new PrintWriter(socket.getOutputStream(), true);

			// send the message

			clOut.println(this.id + ":" + msgType + ":" + localTime + ":" + fileName);
			clOut.println("EOM");
			if (msgType == 4) {
				System.out.println("Last request sent at : " + localTime);
				lastReqTime = localTime;
				// replyForReq = false;
			}
			// close all streams & sockets
			clOut.close();
			socket.close();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			try {
				Thread.sleep(1000);
				sendMessage(host, port, msgType, fileName);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}
		}

	}

	private boolean checkAllReplied() {
		boolean ret = true;
		for (boolean x : repliedArray) {
			ret = x && ret;
		}

		return ret;
	}

	private static int getRandom(int n) {
		double v = Math.random() * n;
		String intval = String.valueOf(v).substring(0,
				String.valueOf(v).indexOf('.'));
		return Integer.parseInt(intval) + 1;
	}
}
